/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package dtdrelacional;

import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.exolab.castor.xml.dtd.parser.*;
import org.exolab.castor.xml.dtd.*;

/**
 */
public class GeneradorTablas {
  //Lista global con todas las tablas generadas a partir del DTD.

  HashMap<String, TablaSQL> tablasSQL;
  HashMap<String, RelacionER> relacionesER;
  DTDdocument dtd;

  public GeneradorTablas(HashMap<String, TablaSQL> tablasSQL,
          HashMap<String, RelacionER> relacionesER) {
    this.tablasSQL = tablasSQL;
    this.relacionesER = relacionesER;
  }

  public void generar(String archDTD) {

    try {
      FileInputStream inputStream = new FileInputStream(archDTD);
      InputStreamReader reader = new InputStreamReader(inputStream, "UTF-8");
      InputCharStream charStream = new InputCharStream(reader);
      DTDParser parser = new DTDParser(charStream);
      dtd = parser.Input(); //Esta es la llamada que parsea el archivo
    } catch (UnsupportedEncodingException ex) {
      Logger.getLogger(GeneradorTablas.class.getName()).log(Level.SEVERE, null, ex);
    } catch (FileNotFoundException ex) {
      Logger.getLogger(GeneradorTablas.class.getName()).log(Level.SEVERE, null, ex);
    } catch (ParseException ex) {
      Logger.getLogger(GeneradorTablas.class.getName()).log(Level.SEVERE, null, ex);
    } catch (DTDException ex) {
      Logger.getLogger(GeneradorTablas.class.getName()).log(Level.SEVERE, null, ex);
    }

    // PROCESO LA RAIZ
    Element raiz = dtd.getElement("database");
    if (raiz == null) {
      // TODO generar error, no hay tag databaser
      return;
    }
    if (!raiz.isElemOnlyContent()) {
      // TODO generar error, la raiz debe contener solo elementos
      return;
    }

    ContentParticle raizCont = raiz.getContent();

    if (raizCont.isSeqType()) {
      // la raiz contiene una secuencia de elementos
      procesarHijosRaiz(raizCont.getChildren());

    } else if (raizCont.isReferenceType()) {
      Element hijo = dtd.getElement(raizCont.getReference());
      if (hijo == null) {
        // TODO error, referencia a un elemento que no existe
        return;
      }

      if (tieneAtributoId(hijo)) {
        // element es entidad porque tiene un atributo como id
        procesarEntidadXML(hijo);
      } else {
        // elemento debe ser relacion
        procesarRelacionXML(hijo);
      }
    } else {
      // TODO error, la raiz solo puede ser secuencia o referencia
    }

    System.out.println("Listo generando tablas.\n");
  }

  private void procesarHijosRaiz(Enumeration hijosRaiz) {
    ContentParticle contentHijo;

    while (hijosRaiz.hasMoreElements()) {
      contentHijo = (ContentParticle) hijosRaiz.nextElement();

      if (contentHijo.isReferenceType()) {
        // proceso el elemento del dtd que se llame como el content
        Element hijo = dtd.getElement(contentHijo.getReference());
        if (hijo == null) {
          // TODO error, referencia a un elemento que no existe
          return;
        }
        if (tieneAtributoId(hijo)) {
          // element es entidad porque tiene un atributo como id
          procesarEntidadXML(hijo);
        } else {
          // elemento debe ser relacion
          procesarRelacionXML(hijo);
        }
      } else {
        //TODO error, los hijos de la raiz solo pueden ser una secuencia
      }
    }
    // TODO procesar todas las relacionesER para incluirlas a las tablas
  }

  private void procesarRelacionXML(Element relacionXML) {

    if (!relacionXML.isElemOnlyContent()) {
      // TODO error, la relacion debe contener elementos hijos
      return;
    }

    ContentParticle relacionContent = relacionXML.getContent();
    if (!relacionContent.isSeqType()) {
      // TODO error, la relacion debe contener al menos dos elementos hijos
      return;
    }

    RelacionER relacionER = new RelacionER(relacionXML.getName());
    relacionesER.put(relacionER.getNombreRelacion(), relacionER);

    Enumeration hijosRel = relacionContent.getChildren();
    ContentParticle contentHijo;
    while (hijosRel.hasMoreElements()) {
      contentHijo = (ContentParticle) hijosRel.nextElement();

      if (!contentHijo.isReferenceType()) {
        //TODO error, los elementos de la relacion deben ser otros elementos
        continue;
      }
      Element hijo = dtd.getElement(contentHijo.getReference());
      if (hijo == null) {
        // TODO error, referencia a un elemento que no existe
        continue;
      }

      if (tieneAtributoId(hijo)) {
        // el hijo es una entidad, se agrega la entidad a relacionER
        if (contentHijo.isZeroOrOneOccurance()) {
          relacionER.addEntidad(contentHijo.getReference(), RelacionER.MINMAX_01);
        } else if (contentHijo.isOneOccurance()) {
          relacionER.addEntidad(contentHijo.getReference(), RelacionER.MINMAX_11);
        } else if (contentHijo.isZeroOrMoreOccurances()) {
          relacionER.addEntidad(contentHijo.getReference(), RelacionER.MINMAX_0N);
        } else if (contentHijo.isOneOrMoreOccurances()) {
          relacionER.addEntidad(contentHijo.getReference(), RelacionER.MINMAX_1N);
        }
        // inserto la entidad
        procesarEntidadXML(dtd.getElement(contentHijo.getReference()));

      } else {
        // el hijo es un atributo de la relacion, se agrega el atributo a la relacionER
        if (hijo.isMixedContent()) {
          if (contentHijo.isOneOccurance()) {
            relacionER.addAtributo(contentHijo.getReference(), true);
          } else if (contentHijo.isZeroOrOneOccurance()) {
            relacionER.addAtributo(contentHijo.getReference(), false);
          } else {
            // TODO error, no se permiten atributos multivaluados para las relaciones
          }
        } else {
          // TODO error, solo se permiten atributos simples para las relaciones
        }
      }
    }

    procesarRelacionSQL(relacionER);
  }

  private void procesarRelacionSQL(RelacionER relacionER) {

    TablaSQL relacionSQL = new TablaSQL(relacionER.getNombreRelacion());

    Iterator<String> iteEntidades = relacionER.getEntidades().iterator();
    String nombreEntidad;
    while (iteEntidades.hasNext()) {
      nombreEntidad = iteEntidades.next();

      // Chequeo si la relacion sera absorbida
      if (relacionER.getMinMaxEntidad(nombreEntidad) == RelacionER.MINMAX_11) {
        relacionER.setAbsorbida(nombreEntidad);
        absorberRelacionSQL(relacionER, nombreEntidad);
        return;
      }

      TablaSQL entidadSQL = tablasSQL.get(nombreEntidad);
      String nombreColumnaPK = entidadSQL.getColumnasPrimarias().iterator().next().getNombre();

      ColumnaSQL columnaFK = new ColumnaSQL(nombreColumnaPK);
      columnaFK.setPrimaria(true);
      columnaFK.setRequerida(true);
      columnaFK.setDatosForanea(nombreEntidad, nombreColumnaPK);
      relacionSQL.addColumna(columnaFK);

    }

    Iterator<String> iteAtributos = relacionER.getAtributos().iterator();
    String nombreAtributo;
    while (iteAtributos.hasNext()) {
      nombreAtributo = iteAtributos.next();
      // TODO agregar los atributos de la relacionER
    }

    // Si termina el ciclo entonces la relacion no se absorbio,
    // entonces agrego la tabla de la relacion a la lista de tablas
    tablasSQL.put(relacionSQL.getNombre(), relacionSQL);

  }

  private void absorberRelacionSQL(RelacionER relacionSQL, String nombreAbsorbedora) {

    TablaSQL entidadSQLAbsorbedora = tablasSQL.get(nombreAbsorbedora);

    // Agrego las claves primarias de las demas entidades en la entidad 'absorbedora'
    Iterator<String> iteEntidades = relacionSQL.getEntidades().iterator();
    String nombreEntidad;
    while (iteEntidades.hasNext()) {
      nombreEntidad = iteEntidades.next();
      // me salto la entidad absorbedora
      if (nombreEntidad.equals(nombreAbsorbedora)) {
        continue;
      }

      TablaSQL entidadSQL = tablasSQL.get(nombreEntidad);
      String nombreColumnaPK = entidadSQL.getColumnasPrimarias().iterator().next().getNombre();

      ColumnaSQL columnaFK = new ColumnaSQL(nombreColumnaPK);
      columnaFK.setPrimaria(false);
      columnaFK.setRequerida(true);
      columnaFK.setDatosForanea(nombreEntidad, nombreColumnaPK);
      entidadSQLAbsorbedora.addColumna(columnaFK);
    }
    // TODO agregar los atributos de la relacionER
  }

  private void procesarEntidadXML(Element entidad) {

    // Me regreso si la entidad ya existe
    if (tablasSQL.containsKey(entidad.getName())) {
      return;
    }

    TablaSQL tablaEntidad = new TablaSQL(entidad.getName());

    // agrego los atributos de ATTLIST
    procesarAttlist(entidad.getAttributes(), tablaEntidad);

    if (entidad.isElemOnlyContent()) {
      procesarContenido(entidad.getContent(), tablaEntidad);
    } else if (entidad.isMixedContent()) {
      // la entidad es pcdata
      ColumnaSQL columna = new ColumnaSQL(entidad.getName());
      tablaEntidad.addColumna(columna);
    } else if (entidad.isAnyContent() || entidad.isEmptyContent()) {
      // TODO error, el elemento no puede ser ANY o EMPTY
      return;
    }
    tablasSQL.put(entidad.getName(), tablaEntidad);
  }

  private void procesarContenido(ContentParticle contenido, TablaSQL tablaPadre) {

    if (contenido.isReferenceType()) {
      procesarContenidoReferencia(contenido, tablaPadre);

    } else if (contenido.isSeqType() || contenido.isChoiceType()) {
      Enumeration hijos = contenido.getChildren();
      ContentParticle hijo;

      while (hijos.hasMoreElements()) {
        hijo = (ContentParticle) hijos.nextElement();

        if (!hijo.isReferenceType()) {
          // TODO error, 
          continue;
        }
        procesarContenidoReferencia(hijo, tablaPadre);
      }

    } else if (contenido.isChoiceType()) {
      // TODO error, no se permite que un contenido se de tipo CHOICE
    }
  }

  private void procesarContenidoReferencia(ContentParticle contenidoHijo,
          TablaSQL tablaPadre) {

    Element elementoHijo = dtd.getElement(contenidoHijo.getReference());
    if (elementoHijo == null) {
      // TODO error, referencia a un elemento que no existe
      return;
    }

    // chequeo que el hijo no sea Entidad
    if (tieneAtributoId(elementoHijo)) {
      // TODO error, no se permiten entidades debajo de otras entidades
      return;
    }

    if (elementoHijo.isMixedContent()) {
      // el elemento es pcdata, entonces lo agrego

      if (contenidoHijo.isOneOccurance()) {
        ColumnaSQL columna = new ColumnaSQL(contenidoHijo.getReference());
        columna.setRequerida(true);

        // Lo siguiente esta muy cableado, pero funciona!
        // Para las tablas que son atributos y no entidades
        Element elementoPadre = dtd.getElement(tablaPadre.getNombre());
        if (elementoPadre == null || !tieneAtributoId(elementoPadre)) {
          columna.setPrimaria(true);
        }

        tablaPadre.addColumna(columna);

      } else if (contenidoHijo.isZeroOrOneOccurance()) {
        ColumnaSQL columna = new ColumnaSQL(contenidoHijo.getReference());
        columna.setRequerida(false);
        tablaPadre.addColumna(columna);

      } else if (contenidoHijo.isZeroOrMoreOccurances() || contenidoHijo.isOneOrMoreOccurances()) {
        // creo una nueva tabla porque el contenidoHijo es multivaluado
        TablaSQL tablaHijo = procesarContenidoMultivaluado(tablaPadre, elementoHijo);
        ColumnaSQL columnaTablaHijo = new ColumnaSQL(elementoHijo.getName());
        columnaTablaHijo.setPrimaria(true);
        columnaTablaHijo.setRequerida(true);
        tablaHijo.addColumna(columnaTablaHijo);
      }

    } else if (elementoHijo.isElemOnlyContent()) {

      if (contenidoHijo.isOneOccurance() || contenidoHijo.isZeroOrOneOccurance()) {
        procesarContenido(elementoHijo.getContent(), tablaPadre);

      } else if (contenidoHijo.isZeroOrMoreOccurances() || contenidoHijo.isOneOrMoreOccurances()) {
        // creo una nueva tabla porque el contenidoHijo es multivaluado
        TablaSQL tablaHijo = procesarContenidoMultivaluado(tablaPadre, elementoHijo);

        procesarContenido(elementoHijo.getContent(), tablaHijo);
      }
    }
  }

  private TablaSQL procesarContenidoMultivaluado(TablaSQL tablaPadre, Element elementoHijo) {
    // creo una nueva tabla porque el contenidoHijo es multivaluado
    String nombreTablaHijo = elementoHijo.getName();
    if (tablasSQL.containsKey(nombreTablaHijo)) {
      nombreTablaHijo += "_" + tablaPadre.getNombre();
    }
    TablaSQL tablaHijo = new TablaSQL(nombreTablaHijo);
    tablasSQL.put(nombreTablaHijo, tablaHijo);

    // coloco la clave primaria a la tablaHijo
    Iterator<ColumnaSQL> ite = tablaPadre.getColumnasPrimarias().iterator();
    while (ite.hasNext()) {
      ColumnaSQL columnaTablaPadre = ite.next();
      ColumnaSQL columnaTablaHijo = new ColumnaSQL(columnaTablaPadre.getNombre());
      columnaTablaHijo.setPrimaria(true);
      columnaTablaHijo.setRequerida(true);
      columnaTablaHijo.setDatosForanea(tablaPadre.getNombre(), columnaTablaPadre.getNombre());
      tablaHijo.addColumna(columnaTablaHijo);
    }

    return tablaHijo;
  }

  /**
   * 
   * @return true si el elemento e tiene id.
   */
  private boolean tieneAtributoId(Element element) {
    Attribute attribute;

    Enumeration attributes = element.getAttributes();
    while (attributes.hasMoreElements()) {
      attribute = (Attribute) attributes.nextElement();

      if (attribute.isIDType()) {
        return true;
      }
    }
    return false;
  }

  private void procesarAttlist(Enumeration attributes, TablaSQL tablaSQL) {
    Attribute attr;

    while (attributes.hasMoreElements()) {
      attr = (Attribute) attributes.nextElement();
      ColumnaSQL columna = new ColumnaSQL(attr.getName());

      if (attr.isIDType()) {
        // el atributo es la clave
        columna.setPrimaria(true);
        columna.setRequerida(true);

      } else if (attr.isStringType()) {
        // el atributo no es clave
        if (attr.isREQUIRED()) {
          columna.setRequerida(true);
        } else if (attr.isIMPLIED()) {
          columna.setRequerida(false);
        } else {
          // TODO error, el valor del atributo solo puede ser REQUIRED o IMPLIED
        }
      } else {
        // TODO error, el atributo solo puede ser de tipo ID o CDATA
        return;
      }
      tablaSQL.addColumna(columna);
    }
  }
}
